package mx.com.crowdgine.display.jogl.shader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Map;

import javax.media.opengl.GL2;
import javax.media.opengl.GL4bc;

import com.jogamp.common.nio.Buffers;

public class ShaderObject {
	
	public static final int SHADER_OBJECT_MAX_VARIABLES = 32;
	
	private GL2 gl;
	private int shaderId;
	private int numVars;
	private int [] variableIds = new int[SHADER_OBJECT_MAX_VARIABLES];
	private IntBuffer [] variableTypes = new IntBuffer[SHADER_OBJECT_MAX_VARIABLES];
	private IntBuffer [] variableSizes = new IntBuffer[SHADER_OBJECT_MAX_VARIABLES];
	private String [] variableNames = new String[SHADER_OBJECT_MAX_VARIABLES];
	private boolean active = false;
	
	public ShaderObject(GL2 gl, String fragmentShader) { //TODO: Make Gl2 GL4BC
		this.gl = gl;
		this.initShaders(fragmentShader);
	}
	
	public ShaderObject(GL2 gl, String fragmentShader, Map<String, Object> dynamicParameters) {
		this.gl = gl;
		this.initShaders(fragmentShader, dynamicParameters);
	}
	
	
	public ShaderObject(GL2 gl, String vertexShader, String fragmentShader) {
		this.gl = gl;
		this.initShaders(vertexShader, fragmentShader);
	}
	
	public ShaderObject(GL4bc gl, String vertexShader, String fragmentShader, String geometryShader){
		this.gl=gl;
		this.initShaders(vertexShader, fragmentShader,geometryShader);
	}
	
	public ShaderObject(GL4bc gl, String vertexShader, String fragmentShader, String geometryShader, String tessEvalShader, String tessControlShader){
		this.gl=gl;
		this.initShaders(fragmentShader, vertexShader, geometryShader, tessControlShader, tessEvalShader);
	}
	
	private void initShaders(String vertexShader, String fragmentShader, String geometryShader, String tessControlShader, String tessEvalShader){
		int progLinkSuccess;
		
		String shader;
		
		int vertexHandler, fragmentHandler, geometryHandler , tessEvalHandler, tessControlHandler;
		int programObject;
		
		shaderId = 0;
		
		shader = loadShaderCode(vertexShader);
		if(shader == null) {
			System.out.println("Unable to load shader " + vertexShader);
			return;
		}
		
		vertexHandler = gl.glCreateShader(GL2.GL_VERTEX_SHADER);
		gl.glShaderSource(vertexHandler, 1, new String[]{shader}, null);
		
		shader = loadShaderCode(fragmentShader);
		if(shader == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		fragmentHandler = gl.glCreateShader(GL2.GL_FRAGMENT_SHADER);
		gl.glShaderSource(fragmentHandler, 1, new String[]{shader}, null);
		
		shader= loadShaderCode(geometryShader);
		
		if(shader==null){
			System.out.println("Unable to load shader " + geometryShader);
			return;
		}
		
		geometryHandler = gl.glCreateShader(GL4bc.GL_GEOMETRY_SHADER);
		gl.glShaderSource(geometryHandler, 1, new String[]{shader}, null);
		
		shader = loadShaderCode(tessEvalShader);
		
		if(shader==null){
			System.out.println("Unable to load shader " + tessEvalShader);
			return;
		}
		
		tessEvalHandler=gl.glCreateShader(GL4bc.GL_TESS_EVALUATION_SHADER);
		gl.glShaderSource(tessEvalHandler, 1, new String[]{shader}, null);
		
		shader = loadShaderCode(tessControlShader);
		
		if(shader==null){
			System.out.println("Unable to load shader " + tessControlShader);
			return;
		}
		
		tessControlHandler=gl.glCreateShader(GL4bc.GL_TESS_CONTROL_SHADER);
		gl.glShaderSource(tessControlHandler, 1, new String[]{shader}, null);
		
		
		gl.glCompileShader(vertexHandler);
		gl.glCompileShader(fragmentHandler);
		gl.glCompileShader(geometryHandler);
		gl.glCompileShader(tessEvalHandler);
		gl.glCompileShader(tessControlHandler);
		
		programObject = gl.glCreateProgram();
		
		gl.glAttachShader (programObject, vertexHandler);
		gl.glAttachShader (programObject, fragmentHandler);
		gl.glAttachShader(programObject, geometryHandler);
		gl.glAttachShader(programObject, tessEvalHandler);
		gl.glAttachShader(programObject, tessControlHandler);
		
		gl.glLinkProgram (programObject);

		gl.glDeleteShader(vertexHandler);
		gl.glDeleteShader(fragmentHandler);
		gl.glDeleteShader(geometryHandler);
		gl.glDeleteShader(tessEvalHandler);
		gl.glDeleteShader(tessControlHandler);
		
		IntBuffer progLinkSuccessBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv (programObject, GL2.GL_LINK_STATUS, progLinkSuccessBuffer);
		
		progLinkSuccess = progLinkSuccessBuffer.get(0);
		
		if(progLinkSuccess == 0) {
			printLinkLog(vertexShader, fragmentShader, programObject); //TODO: add printLinkLog for geometryShader and tess shadders
			return;
		}
		
		System.out.println(vertexShader + ", " + fragmentShader +  ", "  + geometryShader +  ", "  + tessEvalShader+  ", " + tessControlShader + "," + " build succeeded...");
		shaderId = programObject;
		gl.glUseProgram(shaderId);
		numVars = 0;
		initVarInfo();
	}
	
	private void initShaders(String vertexShader, String fragmentShader, String geometryShader){
		int progLinkSuccess;
		
		String shader;
		
		int vertexHandler, fragmentHandler, geometryHandler;
		int programObject;
		
		shaderId = 0;
		
		shader = loadShaderCode(vertexShader);
		if(shader == null) {
			System.out.println("Unable to load shader " + vertexShader);
			return;
		}
		
		vertexHandler = gl.glCreateShader(GL2.GL_VERTEX_SHADER);
		gl.glShaderSource(vertexHandler, 1, new String[]{shader}, null);
		
		shader = loadShaderCode(fragmentShader);
		if(shader == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		fragmentHandler = gl.glCreateShader(GL2.GL_FRAGMENT_SHADER);
		gl.glShaderSource(fragmentHandler, 1, new String[]{shader}, null);
		
		shader= loadShaderCode(geometryShader);
		
		if(shader==null){
			System.out.println("Unable to load shader " + geometryShader);
			return;
		}
		
		geometryHandler = gl.glCreateShader(GL4bc.GL_GEOMETRY_SHADER);
		gl.glShaderSource(geometryHandler, 1, new String[]{shader}, null);
		
		gl.glCompileShader(vertexHandler);
		gl.glCompileShader(fragmentHandler);
		gl.glCompileShader(geometryHandler);
		
		programObject = gl.glCreateProgram();
		
		gl.glAttachShader (programObject, vertexHandler);
		gl.glAttachShader (programObject, fragmentHandler);
		gl.glAttachShader(programObject, geometryHandler);
		
		gl.glLinkProgram (programObject);

		gl.glDeleteShader(vertexHandler);
		gl.glDeleteShader(fragmentHandler);
		gl.glDeleteShader(geometryHandler);
		
		IntBuffer progLinkSuccessBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv (programObject, GL2.GL_LINK_STATUS, progLinkSuccessBuffer);
		
		progLinkSuccess = progLinkSuccessBuffer.get(0);
		
		if(progLinkSuccess == 0) {
			printLinkLog(vertexShader, fragmentShader, programObject); //TODO: add printLinkLog for geometryShader
			return;
		}
		
		System.out.println(vertexShader + ", " + fragmentShader +  ", "  + geometryShader + " build succeeded...");
		shaderId = programObject;
		gl.glUseProgram(shaderId);
		numVars = 0;
		initVarInfo();
	}
	
	public ShaderObject(GL2 gl, String vertexShader, String fragmentShader, Map<String, Object> dynamicParameters) {
		this.gl = gl;
		this.initShaders(vertexShader, fragmentShader, dynamicParameters);
	}
	
	
	private void initShaders(String vertexShader, String fragmentShader) {
		
		String vertexShaderCode;
		
		vertexShaderCode = loadShaderCode(vertexShader);
		if(vertexShaderCode == null) {
			System.out.println("Unable to load shader " + vertexShader);
			return;
		}
		
		String fragmentShaderCode = null;
		
		fragmentShaderCode = loadShaderCode(fragmentShader);
		if(fragmentShaderCode == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		initFragmentAndVertexShaderCode(vertexShader, fragmentShader,
				vertexShaderCode, fragmentShaderCode);
	}
	
	
	
	private void initShaders(String vertexShader, String fragmentShader, Map<String, Object> dynamicParameters) {
		
		String vertexShaderCode;
		
		vertexShaderCode = loadShaderCode(vertexShader);
		
		if(vertexShaderCode == null) {
			System.out.println("Unable to load shader " + vertexShader);
			return;
		}
		
		
		
		String fragmentShaderCode = null;
		
		fragmentShaderCode = loadShaderCode(fragmentShader);
		if(fragmentShaderCode == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		
		
		for(String parameterName : dynamicParameters.keySet()) {
			vertexShaderCode = vertexShaderCode.replaceAll(parameterName, dynamicParameters.get(parameterName).toString());
			fragmentShaderCode = fragmentShaderCode.replaceAll(parameterName, dynamicParameters.get(parameterName).toString());
		}
		
		System.out.println("Vertex Shader Code loaded: " + vertexShader);
		System.out.println(vertexShaderCode);
		System.out.println("Fragment Shader Code loaded:" + fragmentShader);
		System.out.println(fragmentShaderCode);
		
		initFragmentAndVertexShaderCode(vertexShader, fragmentShader,
				vertexShaderCode, fragmentShaderCode);
	}
	
	

	public void initFragmentAndVertexShaderCode(String vertexShader,
			String fragmentShader, String vertexShaderCode,
			String fragmentShaderCode) {
		int progLinkSuccess;
		int vertexHandler, fragmentHandler;
		int programObject;
		
		shaderId = 0;
		
		vertexHandler = gl.glCreateShader(GL2.GL_VERTEX_SHADER);
		gl.glShaderSource(vertexHandler, 1, new String[]{vertexShaderCode}, null);
		
		fragmentHandler = gl.glCreateShader(GL2.GL_FRAGMENT_SHADER);
		gl.glShaderSource(fragmentHandler, 1, new String[]{fragmentShaderCode}, null);
		
		gl.glCompileShader(vertexHandler);
		gl.glCompileShader(fragmentHandler);
		
		programObject = gl.glCreateProgram();
		
		gl.glAttachShader (programObject, vertexHandler);
		gl.glAttachShader (programObject, fragmentHandler);
		
		gl.glLinkProgram (programObject);

		gl.glDeleteShader(vertexHandler);
		gl.glDeleteShader(fragmentHandler);
		
		IntBuffer progLinkSuccessBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv (programObject, GL2.GL_LINK_STATUS, progLinkSuccessBuffer);
		
		progLinkSuccess = progLinkSuccessBuffer.get(0);
		
		if(progLinkSuccess == 0) {
			printLinkLog(vertexShader, fragmentShader, programObject);
			return;
		}
		
		System.out.println(vertexShader + ", " + fragmentShader + " build succeeded...");
		shaderId = programObject;
		gl.glUseProgram(shaderId);
		numVars = 0;
		initVarInfo();
	}


	private void printLinkLog(String vertexShader, String fragmentShader,
			int programObject) {
		IntBuffer infoLogLengthBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv (programObject, GL2.GL_INFO_LOG_LENGTH, infoLogLengthBuffer);
		
		int infoLogLength = infoLogLengthBuffer.get(0);
		
		ByteBuffer byteBuffer = Buffers.newDirectByteBuffer(infoLogLength);
		gl.glGetProgramInfoLog (programObject, infoLogLength, null, byteBuffer);
		
		byte [] bytes = new byte[infoLogLength];
		byteBuffer.get(bytes);
		
		String log = new String(bytes);
		System.out.println(log);
		System.out.println("Shader could not be linked " + vertexShader + ", " + fragmentShader);
	}
	
	
	private void initShaders(String fragmentShader) {
		
		String shader = null;
		
		shader = loadShaderCode(fragmentShader);
		
		if(shader == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		initFragmentShaderCode(fragmentShader, shader);
		
		
	}
	
	
	private void initShaders(String fragmentShader, Map<String, Object> dynamicParameters) {
		
		String shader = null;
		
		shader = loadShaderCode(fragmentShader);
		
		for(String parameterName : dynamicParameters.keySet()) {
			shader = shader.replaceAll(parameterName, dynamicParameters.get(parameterName).toString());
		}
		
		if(shader == null) {
			System.out.println("Unable to load shader " + fragmentShader);
			return;
		}
		
		initFragmentShaderCode(fragmentShader, shader);
		
		
	}
	

	public void initFragmentShaderCode(String fragmentShader, String shader) {
		int fragmentHandler = 0;
		int programObject = 0;
		int progLinkSuccess;
		
		fragmentHandler = gl.glCreateShader(GL2.GL_FRAGMENT_SHADER);
		gl.glShaderSource(fragmentHandler, 1, new String[] {shader}, null);
		gl.glCompileShader(fragmentHandler);
		
		programObject = gl.glCreateProgram();
		gl.glAttachShader(programObject, fragmentHandler);
		gl.glLinkProgram(programObject);
		gl.glDeleteShader(fragmentHandler);
		
		IntBuffer progLinkSuccessBuffer =  Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv(programObject, GL2.GL_LINK_STATUS, progLinkSuccessBuffer);
		
		progLinkSuccess = progLinkSuccessBuffer.get(0);
		
		if(progLinkSuccess == 0) {
			printLinkLog("", fragmentShader, programObject);
			return;
		}
		
		System.out.println(fragmentShader + " build succeded... ");
		shaderId = programObject;
		gl.glUseProgram(shaderId);
		numVars = 0;
		initVarInfo();
	}
	
	
	
	private void initVarInfo() {
		int len, bufLen;
		
		if(shaderId == 0)
			return;
		
		numVars = 0;
		
		IntBuffer numVarsBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv(shaderId, GL2.GL_ACTIVE_UNIFORMS, numVarsBuffer);
		numVars = numVarsBuffer.get(0);
		
		IntBuffer bufLenBuffer = Buffers.newDirectIntBuffer(1);
		gl.glGetProgramiv(shaderId, GL2.GL_ACTIVE_UNIFORM_MAX_LENGTH, bufLenBuffer);
		bufLen = bufLenBuffer.get(0);
		
		IntBuffer lenBuffer = null;
		
		ByteBuffer byteBuffer = null;
		byte [] bytes = null;
		
		for(int i = 0; i < numVars; i++) {
			variableIds[i] = i;
			
			lenBuffer = Buffers.newDirectIntBuffer(1);
			variableSizes[i] = Buffers.newDirectIntBuffer(1);
			variableTypes[i] = Buffers.newDirectIntBuffer(1);
			byteBuffer = Buffers.newDirectByteBuffer(bufLen);
			gl.glGetActiveUniform(shaderId, i, bufLen, lenBuffer, variableSizes[i], variableTypes[i], byteBuffer);
			
			len = lenBuffer.get(0);
			bytes = new byte[len];
			byteBuffer.get(bytes, 0, len);
			
			variableNames[i] = new String(bytes);
		}
		
	}
	
	
	
	private String loadShaderCode(String fileName) {
		String code = "";
		
		try {
			BufferedReader in = new BufferedReader(new FileReader(new File(fileName)));
			String line = in.readLine();
			
			while(line != null) {
				code += line + "\n";
				line = in.readLine();
			}
			
		} catch(Exception ex) {
			System.err.println("Error loading shader code");
			ex.printStackTrace();
		}
		
		return code;
	}
	
	
	
	
	public void setUniformi(String name, int value) {
		
		int variableId = getVariableId(name);
		if(variableId != -1)
			gl.glUniform1i(variableId, value);
	}
	
	
	
	public void setUniformf(String name, float value) {
		int variableId = getVariableId(name);
		if(variableId != -1)
			gl.glUniform1f(variableId, value);
	}
	
	
	
	public void setUniformiv(String name, int [] value, int size) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		switch(size) {
		case 1:
			gl.glUniform1iv(i, 1, Buffers.newDirectIntBuffer(value));
			break;
		case 2:
			gl.glUniform2iv(i, 1, Buffers.newDirectIntBuffer(value));
			break;
		case 3:
			gl.glUniform3iv(i, 1, Buffers.newDirectIntBuffer(value));
			break;
		case 4:
			gl.glUniform4iv(i, 1, Buffers.newDirectIntBuffer(value));
			break;
			
		}
		
	}
	
	
	public void setUniformiv(String name, int [] value, int size, int length) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		switch(size) {
		case 1:
			gl.glUniform1iv(i, length, Buffers.newDirectIntBuffer(value));
			break;
		case 2:
			gl.glUniform2iv(i, length, Buffers.newDirectIntBuffer(value));
			break;
		case 3:
			gl.glUniform3iv(i, length, Buffers.newDirectIntBuffer(value));
			break;
		case 4:
			gl.glUniform4iv(i, length, Buffers.newDirectIntBuffer(value));
			break;
			
		}
		
	}
	
	
	
	
	public void setUniformiv(String name, IntBuffer data, int size, int length) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		switch(size) {
		case 1:
			gl.glUniform1iv(i, length, data);
			break;
		case 2:
			gl.glUniform2iv(i, length, data);
			break;
		case 3:
			gl.glUniform3iv(i, length, data);
			break;
		case 4:
			gl.glUniform4iv(i, length, data);
			break;
			
		}
		
	}
	
	
	
	public void setUniformfv(String name, float [] value, int size) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		switch(size) {
		case 1:
			gl.glUniform1fv(i, 1, Buffers.newDirectFloatBuffer(value));
			break;
		case 2:
			gl.glUniform2fv(i, 1, Buffers.newDirectFloatBuffer(value));
			break;
		case 3:
			gl.glUniform3fv(i, 1, Buffers.newDirectFloatBuffer(value));
			break;
		case 4:
			gl.glUniform4fv(i, 1, Buffers.newDirectFloatBuffer(value));
			break;
			
		}
	}
	
	
	
	public void setUniformfv(String name, float [] value, int size, int length) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		FloatBuffer data = Buffers.newDirectFloatBuffer(value);
		
		switch(size) {
		case 1:
			gl.glUniform1fv(i, length, data);
			break;
		case 2:
			gl.glUniform2fv(i, length, data);
			break;
		case 3:
			gl.glUniform3fv(i, length, data);
			break;
		case 4:
			gl.glUniform4fv(i, length, data);
			break;
			
		}
		
		data.clear();
		
	}
	
	
	public void setUniformfv(String name, FloatBuffer data, int size, int length) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
//		data.rewind();
		
		switch(size) {
		case 1:
			gl.glUniform1fv(i, length, data);
			break;
		case 2:
			gl.glUniform2fv(i, length, data);
			break;
		case 3:
			gl.glUniform3fv(i, length, data);
			break;
		case 4:
			gl.glUniform4fv(i, length, data);
			break;
			
		}
		
	}
	
	
	
	
	public void setUniformMatrix(String name, float [] value, int size) {
		int i = gl.glGetUniformLocation(shaderId, name);
		
		switch(size) {
		case 2:
			gl.glUniformMatrix2fv(i, 1, false, Buffers.newDirectFloatBuffer(value));
			break;
		case 3:
			gl.glUniformMatrix3fv(i, 1, false, Buffers.newDirectFloatBuffer(value));
			break;
		case 4:
			gl.glUniformMatrix4fv(i, 1, false, Buffers.newDirectFloatBuffer(value));
			break;
			
		}
	}
	
	
	public void activate() {
//		if(!active)
			gl.glUseProgramObjectARB(shaderId);
//		active = true;
	}
	
	
	public void deactivate() {
		gl.glUseProgramObjectARB(0);
//		active = false;
	}
	
	
	public void setBindableUniform(String name, int buffer) {
		int variableId = getVariableId(name);
		if(variableId != -1) {
			gl.glUniformBufferEXT(shaderId, variableId, buffer);
		}
	}
	
	
	public void unSetBindableUniform(String name) {
		int variableId = getVariableId(name);
		
		if(variableId != -1) {
			gl.glUniformBufferEXT(shaderId, variableId, 0);
		}
	}
	
	
	public int getBindableUniformSize(String name) {
		int variableId = getVariableId(name);
		if(variableId != -1) {
			return gl.glGetUniformBufferSizeEXT(shaderId, variableId);
		}
		return 0;
	}
	
	
	public void getMaxBindableUniformSize(IntBuffer val) {
		gl.glGetIntegerv(GL2.GL_MAX_BINDABLE_UNIFORM_SIZE_EXT, val);
	}
	
	public void getMaxVertexBindableUniforms(IntBuffer val) {
		gl.glGetIntegerv(GL2.GL_MAX_VERTEX_BINDABLE_UNIFORMS_EXT, val);
	}
	
	public void getMaxFragmentBindableUniforms(IntBuffer val) {
		gl.glGetIntegerv(GL2.GL_MAX_FRAGMENT_BINDABLE_UNIFORMS_EXT, val);
	}
	
	
	public void getMaxGeometryBindableUniforms(IntBuffer val) {
		gl.glGetIntegerv(GL2.GL_MAX_GEOMETRY_BINDABLE_UNIFORMS_EXT, val);
	}
	


	private int getVariableId(String name) {
		int variableId = -1;
		
		for(int i = 0; i < numVars; i++) {
			if(name.equals(variableNames[i])) {
				variableId = variableIds[i];
			}
		}
		return variableId;
	}


	public GL2 getGl() {
		return gl;
	}


	public void setGl(GL2 gl) {
		this.gl = gl;
	}
	
	
	
	
	
}
